package com.jxtc.enterprise.employee.serviceImpl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result;
import com.github.binarywang.wxpay.bean.result.WxPayOrderQueryV3Result;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.jxtc.enterprise.common.config.CompanyPickupCodeConfigProperties;
import com.jxtc.enterprise.common.constants.*;
import com.jxtc.enterprise.common.entity.*;
import com.jxtc.enterprise.common.exception.CancelOrderFailureException;
import com.jxtc.enterprise.common.exception.InventoryUpdateFailureException;
import com.jxtc.enterprise.common.exception.ModifyFailureException;
import com.jxtc.enterprise.common.mapper.*;
import com.jxtc.enterprise.common.utils.*;
import com.jxtc.enterprise.common.vo.DeliverBoxApiParamVO;
import com.jxtc.enterprise.common.vo.OrderLockerBoxNoInfoForCreateOrderVo;
import com.jxtc.enterprise.employee.dto.FoodStockDTO;
import com.jxtc.enterprise.employee.dto.OrderCreateDTO;
import com.jxtc.enterprise.employee.vo.ShoppingCartVO;
import com.jxtc.enterprise.employee.vo.WxPayVo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

import java.math.BigDecimal;
import java.text.MessageFormat;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;

@Service
@Slf4j
@RequiredArgsConstructor
public class JxOrderServiceImpl {

    private final OrderMapper orderMapper;
    private final ShoppingCartMapper shoppingCartMapper;
    private final OrderDetailMapper orderDetailMapper;
    private final UserMapper userMapper;
    private final CompanyMapper companyMapper;
    private final StoreMapper storeMapper;
    private final DepartmentMapper departmentMapper;
    private final TransactionRecordMapper transactionRecordMapper;
    private final RedisUtil redisUtil;
    private final JxWxPayServiceImpl wxPayServiceImpl;
    private final MenuStoreConfigMapper menuStoreConfigMapper;
    private final DeliverLockerMapper deliverLockerMapper;
    private final CompanyPickupCodeConfigProperties companyPickupCodeConfigProperties;
    private final UserOtherCompRelationMapper userOtherCompRelationMapper;

    @Autowired
    private FoodMenuMapper foodMenuMapper;

    private Integer pickupCodeCounter = 1;

    // TODO: 统一管理 Redis 中 key 命名规则
    private final String WX_PRE_PAY_INFO_KEY = "jxtc_order_wx_prepay_info_";

    @Value("${createOrder.canOrderAllTime}")
    private String canOrderAllTimePhoneListStr;

    /**
     * 获取购物车中的每一个菜品项，如果前端传入的购物车 ID 不存在，将抛出异常
     *
     * @param shoppingCartIds 购物车中的每个菜品项 ID
     * @return 购物车中的菜品项列表
     */
    public List<ShoppingCart> listShoppingCartByIds(List<String> shoppingCartIds) {
        List<ShoppingCart> shoppingCarts = new ArrayList<>();
        for (String shoppingCartId : shoppingCartIds) {
            ShoppingCart shoppingCart = shoppingCartMapper.selectById(shoppingCartId);
            // 如果购物车中的菜品项为空或者已被删除，说明前端传递过来的 shoppingCartId 是有问题的，应该抛出异常
            if (shoppingCart == null || shoppingCart.getDelFlag()) {
                throw new IllegalArgumentException("ID 为：" + shoppingCartId + " 的购物车不存在，请联系工作人员进行处理");
            }
            shoppingCarts.add(shoppingCart);
        }
        return shoppingCarts;
    }

    /**
     * 涉及规格的菜品，比如 自选套餐，选择不同规格时，价格是否计算正确
     */
    public void checkOrderPrice(List<ShoppingCart> shoppingCarts, OrderCreateDTO dto) {
        int totalPrice = 0;
        int totalShowPrice = 0;
        for (ShoppingCart shoppingCart : shoppingCarts) {
            totalPrice += shoppingCart.getSalePrice();
            totalShowPrice += shoppingCart.getShowPrice() * shoppingCart.getCount();
        }

        // 如果前端传递过来的 totalPrice 与计算得出的 totalPrice 不一致，抛出异常
        int frontendTotalPrice = CurrencyUtil.yuanToFen(dto.getTotalPrice());
        if (totalPrice != frontendTotalPrice) {
            log.error("totalPrice: {}, 前端传递的 totalPrice: {}", totalPrice, frontendTotalPrice);
            throw new IllegalArgumentException("支付失败：传入订单总价不正确");
        }

        // 如果前端传递过来的 totalShowPrice 与计算得出的 totalShowPrice 不一致，抛出异常
        int frontendTotalShowPrice = CurrencyUtil.yuanToFen(dto.getTotalShowPrice());
        if (totalShowPrice != frontendTotalShowPrice) {
            log.error("totalShowPrice: {}, 前端传递的 totalShowPrice: {}", totalShowPrice, frontendTotalShowPrice);
            throw new IllegalArgumentException("支付失败：传入订单总价不正确");
        }

        // 如果前端传递过来的余额支付金额 + 微信支付金额不等于餐品显示金额的总价，抛出异常
        if (dto.getWalletPay().add(dto.getRealPayPrice()).compareTo(dto.getTotalShowPrice()) != 0) {
            log.error("walletPay: {}, realPayPrice: {}, totalPrice: {}, totalShowPrice: {}", dto.getWalletPay(), dto.getRealPayPrice(), dto.getTotalPrice(), dto.getTotalShowPrice());
            throw new IllegalArgumentException("支付失败：余额支付金额 + 微信支付金额不等于订单总金额");
        }
    }

    /**
     * (1) 对用户选购的菜品是否有库存进行校验，若没有库存则将购物车中的菜品项添加到列表中
     * (2) 若有库存，则将库存减去 订单份数，注意某些订单中，同一份菜品会订购多份，因此要减少实际的菜品份数，而不是减1
     *
     * @param menuId        菜单 ID
     * @param shoppingCarts 购物车中的每个菜品项
     * @return 购物车中没有库存的菜品项
     */
    public List<ShoppingCartVO> checkFoodStock(String menuId, List<ShoppingCart> shoppingCarts) {
        List<ShoppingCartVO> outStockFoods = new ArrayList<>();

        List<FoodStockDTO> foodStockDTOList = new ArrayList<>();
        for (ShoppingCart shoppingCart : shoppingCarts) {
            // 用 menuId 拼接成 redis 中菜品库存的 key
            // store_xxx_stock -> {"food1": 10, "food2":20, "food3": 3, "food4": 0};
            String key = "store_" + menuId + "_stock";
            // TODO: 确定 redis 中存入的菜品名称命名方式，shopping_cart 表中的 food_print_name 字段只有规格拼接，没有菜品名称
            // 暂定菜品名称为 shopping_cart 中 food_name 和 food_print_name 的拼接
            String item = shoppingCart.getFoodName() + shoppingCart.getFoodPrintName();

            log.info("redis 中获取菜品库存，key: {}, item: {}", key, item);

            Integer limitCount = (Integer) redisUtil.hget(key, item);
            // 如果 Redis 中没有对应的 key ，认为该商品没有库存
            if (limitCount == null || limitCount < shoppingCart.getCount()) {
                outStockFoods.add(new ShoppingCartVO(shoppingCart));
            }

            // 将菜品库存信息存入列表中，用于后续更新 Redis 中的库存数据
            foodStockDTOList.add(new FoodStockDTO(key, item, limitCount, shoppingCart.getCount()));
        }

        // 购物车中全部菜品的库存充足的情况下，更新 Redis 中的库存数据
        if (outStockFoods.isEmpty()) {
            for (FoodStockDTO dto : foodStockDTOList) {
                boolean updateLimitCountFlag = redisUtil.hset(dto.getKey(), dto.getItem(), dto.getLimitCount() - dto.getPurchaseCount());
                if (!updateLimitCountFlag) {
                    throw new InventoryUpdateFailureException("库存更新失败，请联系管理员进行排查");
                }
            }
        }

        return outStockFoods;
    }

    @Transactional(rollbackFor = Exception.class)
    public WxPayVo createOrder(List<ShoppingCart> shoppingCarts, OrderCreateDTO dto, OrderLockerBoxNoInfoForCreateOrderVo lockerBoxNoInfoForCreateOrderVo) throws WxPayException {
        // 1. 查询用户信息
        User user = validateAndGetUser(dto);

        // 2. 向数据库中新增订单支付及详情相关信息
        OrderPayment newOrderPayment = insertOrderRecords(user.getId(), user.getCompanyId(), shoppingCarts, dto);
        String orderId = newOrderPayment.getId();
        // 餐食类型，格式: "午餐 15:00 ~ 11:00"
        String mealFlag = newOrderPayment.getMealFlag();
        String mealType = mealFlag.split(" ")[0];
        // 根据 mealFlag 中的 "午餐" 获取对应的英文名称 "lunch"
        String mealTypeEn = getMealTypeEnglishNameFromEnum(mealType);

        // 3. 根据实际支付金额，判断是否调用微信支付接口
        if (dto.getRealPayPrice().compareTo(BigDecimal.ZERO) == 0) {
            // 当钱包余额较多，超过订单金额时，不需要调用微信支付接口，只需要更新表中状态，抵扣钱包余额即可
            log.info("使用钱包余额抵扣所有订单费用，不需要调用微信支付接口");
            // 将 order_payment 和 order_detail 表中的订单状态改成 toSend，已支付待收货
            updateOrderState(orderId);

            // 如果使用余额支付，则将用户可用余额减少指定金额
            updateWalletAfterOrderPayed(newOrderPayment, user);

            // 在 transaction_records 中创建交易记录
            createTransactionRecords(newOrderPayment, user);

            // TODO: 订单支付完成后，更新各个店铺的销量
            updateStoreSalesScores(orderId);

            log.info("不需要微信支付，下单后直接打印，订单id：{}，用户手机号: {}, 公司id: {}， 打印方式为：{}",
                    newOrderPayment.getId(), newOrderPayment.getPhone(), newOrderPayment.getCompanyId(), newOrderPayment.getBatchPrinted());

            // 使用余额支付，其他环节都完成后，计算此订单需要向店主支付的货款，并回写到 order_payment 和 order_detail 表
            updatePayForMerchant(orderId, mealTypeEn);

            // 将 购物车中的菜品 标记删除，将 del_flag 设置为 1;
            deleteShoppingCartItemsByFlag(orderId, dto.getShoppingCartIds());

            WxPayVo walletPointsPayResult = new WxPayVo();
            walletPointsPayResult.setUserId(user.getId())
                    .setDescription("订单费用全部由余额抵扣，不涉及微信支付平台接口调用")
                    .setOutTradeNo(orderId)
                    .setAmountCurrency("CNY")
                    .setAmountTotal(0);
            // NA  not applicable 不适用，由于没有发起微信支付请求，因此返回值中，与微信支付相关的字段都不适用当前场景
            walletPointsPayResult.setAppid("NA")
                    .setMchId("NA")
                    .setTimeStamp("NA")
                    .setNonceStr("NA")
                    .setPackageValue("NA")
                    .setSignType("NA")
                    .setPaySign("NA");

            // 将分配外卖柜编号的参数，通过入参的方式传递出去
            lockerBoxNoInfoForCreateOrderVo.setOrderId(orderId);
            lockerBoxNoInfoForCreateOrderVo.setCompanyId(newOrderPayment.getCompanyId());
            lockerBoxNoInfoForCreateOrderVo.setDateFlag(newOrderPayment.getDateFlag());
            lockerBoxNoInfoForCreateOrderVo.setMealTypeEn(mealTypeEn);

            // 如果当前公司配置一单一打印，则打印机打印订单
            if (BatchPrintEnum.ONLY_SINGLE_PRINT_SUPPORT.getValue().equals(newOrderPayment.getBatchPrinted())) {
                printOrder(orderId);
            }

            return walletPointsPayResult;
        } else {
            log.info("微信支付，此处不打印订单，订单id: {}， 用户：{}，公司：{}，打印方式为：{}",
                    newOrderPayment.getId(), newOrderPayment.getPhone(), newOrderPayment.getCompanyId(), newOrderPayment.getBatchPrinted());
            int realPayPrice = CurrencyUtil.yuanToFen(dto.getRealPayPrice());
            // 调用微信支付接口
            WxPayVo jxWxPayVo = wxPayServiceImpl.pay(user.getId(), user.getOpenId(), orderId, "深食堂餐品订单费用", realPayPrice, "order");

            //  再写入到 redis 中，设置有效期为 2 分钟；当在 callback 中确认用户已支付完成后，也可以提前删除 redis 中的预支付数据
            String wxPrePayInfo = JSONObject.toJSONString(jxWxPayVo);
            String key = WX_PRE_PAY_INFO_KEY + newOrderPayment.getId();
            // 120 s，缓存 预支付信息的作用在于，用户暂时不想购买了，后期有改变想法，简化支付流程；但是 缓存预支付信息，概率性会导致 “支付信息不一致”导致无法支付，因此，将缓存时间设置为5分钟，减小 不一致导致的支付失败的影响
            // 2分钟时间很短，缓存时间过后，预支付信息被删除，再次重新发起请求支付流程即可
            long time = Duration.ofMinutes(2).getSeconds();
            if (!redisUtil.set(key, wxPrePayInfo, time)) {
                log.error("redis 设置预支付信息失败，key: {}, value: {}", key, wxPrePayInfo);
            }

            // 将 购物车中的菜品 标记删除，将 del_flag 设置为 1;
            // 并将购物车中菜品项所属订单id，回填到购物项的 orderId，此字段更多的是用来调试，此字段对于业务方面的作用较小
            deleteShoppingCartItemsByFlag(orderId, dto.getShoppingCartIds());
            return jxWxPayVo;
        }
    }

    /**
     * 通过手机号和公司 ID 查询最新的未删除用户信息
     */
    private User validateAndGetUser(OrderCreateDTO dto) {
        String phone = dto.getPhone();
        LambdaQueryWrapper<User> userQuery = new LambdaQueryWrapper<User>()
                .eq(User::getPhoneNumber, phone)
                //.eq(User::getCompanyId, dto.getCompanyId())
                .eq(User::getDelFlag, 0)
                .orderByDesc(User::getCreateTime);

        return userMapper.selectList(userQuery).stream()
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("手机号为 " + phone + " 的用户不存在"));
    }

    /**
     * 当接收到微信支付平台的 回调消息时，从回调消息中解析出 支付结果，若支付成功，则将订单状态从 待支付，更新成 已支付待配送
     *
     * @param wxPayOrderNotifyV3Result 微信支付平台回调消息
     */
    @Transactional(rollbackFor = Exception.class)
    public void handleOrderNotify(WxPayOrderNotifyV3Result wxPayOrderNotifyV3Result) {
        if (JxWxPayStateEnum.SUCCESS.getValue().equals(wxPayOrderNotifyV3Result.getResult().getTradeState())) {
            String tag = wxPayOrderNotifyV3Result.getResult().getAttach();
            if (tag.equalsIgnoreCase("recharge")) {
                // TODO: 使用微信充值相关业务逻辑暂未支持
                String rechargeTransId = wxPayOrderNotifyV3Result.getResult().getOutTradeNo();
                log.info("充值订单 {} 支付状态为 {}，支付完成，支付详细结果: {}", rechargeTransId, wxPayOrderNotifyV3Result.getResult().getTradeState(), wxPayOrderNotifyV3Result);
                TransactionRecord userTransactionsDo = transactionRecordMapper.selectById(rechargeTransId);
                handleRechargeSuccessResult(userTransactionsDo.getFromId(), rechargeTransId, userTransactionsDo.getAmount());
            } else {
                // 订单支付成功，更新订单状态
                String orderId = wxPayOrderNotifyV3Result.getResult().getOutTradeNo();
                log.info("订单{} 支付状态为 {}，支付完成，支付详细结果: {}", orderId, wxPayOrderNotifyV3Result.getResult().getTradeState(), wxPayOrderNotifyV3Result);
                // 将 order_payment 和 order_detail 表中的订单状态改成 toSend，已支付待收货
                updateOrderState(orderId);

                // 如果使用余额支付，则将用户可用余额减少指定金额
                OrderPayment orderPayment = orderMapper.selectById(orderId);
                User savedUser = userMapper.selectById(orderPayment.getUserId());
                updateWalletAfterOrderPayed(orderPayment, savedUser);

                // 在 transaction_records 中创建交易记录
                createTransactionRecords(orderPayment, savedUser);

                // TODO: 订单支付完成后，更新各个店铺的销量
                updateStoreSalesScores(orderId);
                log.info("微信支付完成后，回调方法中，订单id：{}，公司：{}，用户：{}，打印方式为：{}",
                        orderId, orderPayment.getCompanyId(), orderPayment.getPhone(), orderPayment.getBatchPrinted());


                // 餐食类型，格式: "午餐 15:00 ~ 11:00"
                String mealFlag = orderPayment.getMealFlag();
                String mealType = mealFlag.split(" ")[0];
                // 根据 mealFlag 中的 "午餐" 获取对应的英文名称 "lunch"
                String mealTypeEn = getMealTypeEnglishNameFromEnum(mealType);

                // 计算需要此订单需要向店铺店主支付的货款，将需要向店铺店主支付的货款填充到 order_payment 表和 order_detail 表
                updatePayForMerchant(orderId, mealTypeEn);
                // 支付成功后，为用户的餐品分配 货道编号
                assignLockerBoxNoForOrder(orderId, orderPayment.getCompanyId(), orderPayment.getDateFlag(), mealTypeEn);

                // 删除redis中缓存的预支付信息
                String key = WX_PRE_PAY_INFO_KEY + orderId;
                redisUtil.del(key);

                // 如果当前公司配置一单一打印，则打印机打印订单
                if (BatchPrintEnum.ONLY_SINGLE_PRINT_SUPPORT.getValue().equals(orderPayment.getBatchPrinted())) {
                    printOrder(orderId);
                }
            }
        } else {
            log.error("支付失败，结果 {}", wxPayOrderNotifyV3Result);
        }
    }

    /**
     * 支付成功后，计算需要给店铺店主支付的货款，回填到 order_payment 和 order_detail 表
     *
     * @param orderId
     */
    private void updatePayForMerchant(String orderId, String mealType) {
        // 通过 company_id 和 store_id 以及 food_id 连接 order_detail 和 food_menu 表
        // 使每个订单详情只与具体的食物项相匹配，防止在同一家店铺买多份餐品时，查询出的结果出现重复，导致 totalPayForStore 计算错误
        List<HashMap<String, Object>> storePriceOfEachFoods = orderDetailMapper.queryEachStorePaymentByOrderId(orderId, mealType);
        int totalPayForStore = 0;
        for (HashMap<String, Object> storePriceOfEachFood : storePriceOfEachFoods) {
            String orderDetailId = (String) storePriceOfEachFood.get("orderDetailId");
            Integer foodCount = (Integer) storePriceOfEachFood.get("foodCount");
            Integer storePrice = (Integer) storePriceOfEachFood.get("storePrice");

            // 订单中有多个菜品，每个菜品可能会有多份，此处计算订单中某个菜品有多份时，该菜品需要向店铺店主支付的总金额
            int totalPayForOneFood = storePrice * foodCount;

            orderDetailMapper.update(
                    new LambdaUpdateWrapper<OrderDetail>().
                            eq(OrderDetail::getId, orderDetailId).
                            set(OrderDetail::getStorePrice, totalPayForOneFood));

            totalPayForStore += totalPayForOneFood;
        }
        orderMapper.update(new LambdaUpdateWrapper<OrderPayment>()
                .eq(OrderPayment::getId, orderId)
                .set(OrderPayment::getPayForMerchant, totalPayForStore));
    }

    /**
     * 使用 synchronized 关键字，避免并发问题，导致同一个 货道编号 分配个两个餐品
     * todo 当一份订单，有5份盒饭时，当前实现存在问题，会导致 5份餐品的 order_detail 表中，locker_box_no 都一样
     * 问题背景：dateFlag为 08-13 两份订单，订单id 5f385f760a6c8944a5968333ead75bbe 和 10119f3c508cca326e34e31687b4c83b ，外卖柜编号一样都是 A-7
     * 两份订单的创建时间都是 2025-08-12 19:53:54
     * <p>
     * 注意，此方法需要使用 propagation = Propagation.REQUIRES_NEW；必须在单独的事务中，此方法在 /createOrder 和 /notify 两个接口中调用
     * 两个接口的步骤都很多，而且调用该方法的 两个方法上都使用了 @Transactional 注解；因此 此方法中的 orderDetailMapper.update() 并没有提交，
     * 只有当外层的 com.jxtc.enterprise.employee.serviceImpl.JxOrderServiceImpl#handleOrderNotify
     * 和 com.jxtc.enterprise.employee.serviceImpl.JxOrderServiceImpl#createOrder 结束后，才会提交
     * dateFlag为08-13的两个订单，通过 /createOrder 接口执行此方法，（都是0元下单，因此不是通过 微信支付后的回调 /notify），
     * 两个线程通过 com.jxtc.enterprise.employee.serviceImpl.JxOrderServiceImpl#createOrder 方法调用 assignLockerBoxNoForOrder() 方法，
     * 虽然使用了 synchronized 保证了两个线程中的调用 串行，但是 订单1的线程 执行完此方法后，由于assignLockerBoxNoForOrder() 方法上没有使用 propagation = Propagation.REQUIRES_NEW
     * 而是使用的默认值  Propagation.PROPAGATION_REQUIRED ，导致 assignLockerBoxNoForOrder() 中的 orderDetailMapper.update() 操作还没有提交
     * 当订单2的线程 执行 assignLockerBoxNoForOrder() 查询时，A-7 还没有写入订单1的 order_detail 表中记录的 locker_box_no，导致 A-7 还没有分配，被重复分配了
     * <p>
     * 使用 propagation = Propagation.REQUIRES_NEW 保证了 订单1结束后，事务立即提交，A-7 会被持久化到 order_detail 表中记录的 locker_box_no；
     * 这样当订单2所在的线程获取到锁后，执行sql查询时，A-7已经分配了，就不会再被重复分配
     * <p>
     * synchronized 只能保证 订单2在订单1线程执行完 assignLockerBoxNoForOrder() 方法后，才能执行 assignLockerBoxNoForOrder()此方法；但是不能保证
     * assignLockerBoxNoForOrder() 方法中的update sql 提交，订单2获取到锁后，执行 assignLockerBoxNoForOrder()时，
     * 执行 select sql时，订单1的 locker_box_no 还是为 默认值32个全0，还不是A-7
     *
     * @param orderId
     * @param companyId
     * @param dateFlag
     * @param mealTypeEn lunch, dinner等，用餐时段
     */
    // 编号重复fix方案1新增，修改事务传播行为参数，改为 REQUIRES_NEW --------
    @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_COMMITTED)
    public synchronized String assignLockerBoxNoForOrder(String orderId, String companyId, String dateFlag, String mealTypeEn) {

        log.info("begin_to_evaluate_can_assigned_box_no, companyId: {}, dateFlag: {}, mealTypeEn: {}, orderId: {}", companyId, dateFlag, mealTypeEn, orderId);
        String lockerBoxNo = OrderConstants.DEFAULT_LOCKER_BOX_NO; // 默认值，无效的 货道编号
        String lockerInfo = "{}"; // 默认值，无效的 开货道接口调用入参
        List<String> alreadyAssignedLockerBoxNoList = orderMapper.queryAlreadyAssignedLockerBoxNo(companyId, dateFlag, mealTypeEn);
        List<String> uniqueAlreadyAssignedLockerBoxNoList = alreadyAssignedLockerBoxNoList.stream().map(String::trim).distinct().collect(Collectors.toList());
        // 向 order_detail 表中新增一条记录，locker_box_no 默认值就是 00000000000000000000000000000000，因此需要先移除该编号
        uniqueAlreadyAssignedLockerBoxNoList.remove(OrderConstants.DEFAULT_LOCKER_BOX_NO);
        log.info("already_assigned_locker_box_no count: {}, value: {}", uniqueAlreadyAssignedLockerBoxNoList.size(), JSONObject.toJSONString(uniqueAlreadyAssignedLockerBoxNoList));

        // 根据公司id，查询外卖柜表 delivery_locker，获取该公司绑定的 外卖柜列表
        // 已绑定货柜，companyId 不为 000000000000000000000000000，未绑定为 000000000000000000000000000； 已删除的外卖柜 del_flag 为 1
        List<DeliveryLocker> bindedDeliveryLockers = deliverLockerMapper.selectList(new LambdaQueryWrapper<DeliveryLocker>().eq(DeliveryLocker::getDelFlag, false).eq(DeliveryLocker::getCompanyId, companyId));
        if (CollectionUtils.isEmpty(bindedDeliveryLockers)) {
            // 如果订单所属公司，没有外卖柜，则该订单分配的外卖柜编号为 使用默认值 32个0，订单打印时，实际打印的货道编号是 取餐编号
            log.info("not_bind_locker_for_companyId: {}, no_need_to_assign_locker_box_no_for_orderId: {}", companyId, orderId);
        } else {
            List<String> avaliableLockerBoxNoList = new ArrayList<>();
            List<DeliverBoxApiParamVO> avaliableBoxList = evaltAvaliableDeliverBoxList(bindedDeliveryLockers, avaliableLockerBoxNoList);
            log.info("can_assigned_locker_box_no count: {}, value: {}, ",
                    avaliableBoxList.size(), JSONObject.toJSONString(avaliableBoxList));
            List<DeliverBoxApiParamVO> canAssignedLockerBoxList = new ArrayList<>();
            // 先判断下，已经分配的编号，是否已经超过了可以分配的编号个数，即 已经下单的订单份数是否等于 可用的外卖柜货道个数
            if (uniqueAlreadyAssignedLockerBoxNoList.size() < avaliableBoxList.size()) {
                for (int k = 0; k < avaliableBoxList.size(); k++) {
                    DeliverBoxApiParamVO lockerBox = avaliableBoxList.get(k);
                    // 对根据外卖柜信息，拼接的总外卖柜货道列表中的每一个货道进行判断，若已经分配了，则跳过，若未分配则添加到 可分配列表 canAssignedLockerBoxList 中
                    if (!uniqueAlreadyAssignedLockerBoxNoList.contains(lockerBox.getLockerBoxNo())) {
                        canAssignedLockerBoxList.add(lockerBox);
                    }
                }
            } else {
                // 若 外卖柜货道不够了，则新的订单，使用默认值 32个0，订单打印时，实际打印的货道编号是 取餐编号
                log.error("already assigned locker_box: {}, more than available count: {}, companyId: {}, dateFlag: {}, mealFlag: {}",
                        uniqueAlreadyAssignedLockerBoxNoList.size(), avaliableBoxList.size(), companyId, dateFlag, mealTypeEn);
            }

            // 若可分配列表不为空
            if (!canAssignedLockerBoxList.isEmpty()) {
                // 按照每个货道的 seqNo 进行升序排序
                canAssignedLockerBoxList.sort(Comparator.comparingInt(DeliverBoxApiParamVO::getSeqNo));
                // 排序后，第一个货道即为 可以分配给当前订单的货道
                DeliverBoxApiParamVO assignedLockerBoxVo = canAssignedLockerBoxList.get(0);
                lockerBoxNo = assignedLockerBoxVo.getLockerBoxNo();
                lockerInfo = JSONObject.toJSONString(assignedLockerBoxVo);
            } else {
                lockerBoxNo = OrderConstants.NO_LOCKER_BOX_NO;
                log.error("no_locker_box_available, already assigned locker_box: {}, available count: {}, companyId: {}, dateFlag: {}, mealFlag: {}, use {} as locker_box_no",
                        uniqueAlreadyAssignedLockerBoxNoList.size(), avaliableBoxList.size(), companyId, dateFlag, mealTypeEn, lockerBoxNo);
            }
        }

        // 对应无外卖柜 和 有外卖柜的公司的订单，都会执行到此处，对于没有外卖柜的公司的订单，外卖柜编号使用默认值 32个0
        int updatedCount = orderDetailMapper.update(
                new LambdaUpdateWrapper<OrderDetail>()
                        .set(OrderDetail::getLockerBoxNo, lockerBoxNo)
                        .set(OrderDetail::getLockerInfo, lockerInfo)
                        .set(OrderDetail::getUpdateTime, LocalDateTime.now())
                        .eq(OrderDetail::getOrderId, orderId));
        log.info("assign_locker_box_no: {} for orderId: {}, update count: {}", lockerBoxNo, orderId, updatedCount);
        return lockerBoxNo;
    }

    /**
     * 查询微信支付中订单的支付状态
     *
     * @param orderId 订单 ID
     * @return 订单的支付状态
     */
    public WxPayOrderQueryV3Result queryOrderPayResult(String orderId) {
        return wxPayServiceImpl.queryPayResult(orderId);
    }

    /**
     * 查询未支付的“预支付信息”。对于已经下单，但是终止了支付的订单，此接口可以用来继续之前未完成的支付过程*
     * 从 redis 中获取之前下单时，调用 "发起支付接口" 获取的 预支付信息，继续复用之前的交易信息，当前调用 “发起支付接口”时，设置的 预支付信息有效期为 1天，因此在一天之内，可以复用，不需要再次调用接口，获取新的 预支付信息；
     * 若 redis 中没有，则重新调用微信支付的 发起支付接口，重新获取一次 预支付信息
     *
     * @param orderId 订单 ID
     * @return 微信支付平台返回的预支付信息
     */
    public WxPayVo toPay(String orderId) throws WxPayException {
        String wxPrePayInfo = (String) redisUtil.get(WX_PRE_PAY_INFO_KEY + orderId);
        log.info("topay_get_pay_info_from_redis key: {}, value: {}", WX_PRE_PAY_INFO_KEY + orderId, wxPrePayInfo);
        if (wxPrePayInfo == null || wxPrePayInfo.isEmpty()) {
            // 若 redis 中找不到 预支付信息，则重新 调用下单接口，获取新的预支付信息
            OrderPayment orderPayment = orderMapper.selectById(orderId);
            if (orderPayment == null) {
                throw new IllegalArgumentException("ID 为 " + orderId + " 的订单不存在");
            }

            User user = userMapper.selectById(orderPayment.getUserId());
            return wxPayServiceImpl.pay(user.getId(), user.getOpenId(), orderId, "深食堂餐品订单费用", orderPayment.getUserPaid(), "order");
        }

        return JSON.parseObject(wxPrePayInfo, WxPayVo.class);
    }

    @Transactional(rollbackFor = ModifyFailureException.class)
    public void cancelOrder(String orderId) throws WxPayException {
        OrderPayment orderPayment = orderMapper.selectById(orderId);
        if (orderPayment == null) {
            throw new IllegalArgumentException("ID 为 " + orderId + " 的订单不存在，请联系工作人员进行处理");
        }

        OrderPayment updateOrderPayment = new OrderPayment();
        updateOrderPayment.setId(orderPayment.getId());

        if (Objects.equals(orderPayment.getStatus(), OrderPaymentStatus.WAIT.getValue())) {
            // 取消待支付的订单，即用户还没有支付，则只需要只需要调用微信接口，取消 支付交易；
            abortPay(orderId);
            // 然后 将订单状态更新为 canceled 即可
            updateOrderPayment.setStatus(OrderPaymentStatus.CANCELED.getValue());
        } else if (Objects.equals(orderPayment.getStatus(), OrderPaymentStatus.TO_SEND.getValue())) {
            // 取消已支付待发货的订单，即用户已经支付，但是商家还没有发货，此时需要将订单状态更新为 canceled，同时将用户余额退还给用户；
            User savedUser = userMapper.selectById(orderPayment.getUserId());
            if (savedUser == null) {
                throw new IllegalArgumentException("订单无法取消，ID 为 " + orderPayment.getUserId() + " 的用户不存在，请联系工作人员进行处理");
            }

            // 根据用户支付方式退还用户支付金额
            if (Objects.equals(orderPayment.getPaymentMethod(), JxPaymentMethodEnum.WALLET.getValue())) {
                // 如果用户使用余额进行支付，此时退还用户余额即可
                refundUserBalance(savedUser.getId(), savedUser.getBalance(), orderPayment.getDeductionPrice());
            } else if (Objects.equals(orderPayment.getPaymentMethod(), JxPaymentMethodEnum.WALLET_WXPAY.getValue())
                    || Objects.equals(orderPayment.getPaymentMethod(), JxPaymentMethodEnum.WXPAY.getValue())) {
                // 如果用户使用【余额 + 微信支付】或【仅使用微信支付】的方式，此时调用微信退款接口
                wxPayServiceImpl.refund(orderId, orderPayment.getUserPaid(), orderPayment.getUserPaid());

                // 更新用户余额应该在微信退款接口调用成功后再进行判断，因为微信退款接口调用成功后，才应该更新数据库中的信息
                // refundUserBalance(savedUser.getId(), savedUser.getBalance(), orderPayment.getDeductionPrice());
            } else {
                // 如果用户使用其他支付方式，则无法进行退款操作，请联系工作人员进行处理
                log.error("未知场景，订单详细数据如下：{}，可根据订单中原始金额，实际支付金额，积分钱包余额抵扣金额等信息进行排查", orderPayment);
            }
            // 将订单状态更新为 canceled
            updateOrderPayment.setStatus(OrderPaymentStatus.CANCELED.getValue());
        } else {
            // 不支持取消订单的订单状态
            throw new CancelOrderFailureException("当前订单状态为：" + orderPayment.getStatus() + "，取消订单失败");
        }

        // TODO 1: 这里应该是还有问题的，应该根据退款方式分情况处理，如果支付方式包含微信支付时，应在微信支付退款成功后再更新表状态
        // TODO 2: 其次，应该分情况记录交易记录信息、返回菜品库存信息
        // 更新订单状态，更新失败时抛出异常，统一处理
        if (orderMapper.updateById(updateOrderPayment) <= 0) {
            throw new ModifyFailureException("ID 为 " + orderId + " 的订单取消失败，请联系工作人员进行处理");
        }

        // 更新订单支付表对应的订单详情表中的 status 字段为已取消状态，order_detail 与 order_payment 表中的 status 必须一致
        LambdaUpdateWrapper<OrderDetail> orderDetailUpdateWrapper = new LambdaUpdateWrapper<OrderDetail>()
                .eq(OrderDetail::getOrderId, orderId);

        OrderDetail updateOrderDetail = new OrderDetail();
        updateOrderDetail.setStatus(updateOrderPayment.getStatus());

        if (orderDetailMapper.update(updateOrderDetail, orderDetailUpdateWrapper) <= 0) {
            throw new ModifyFailureException("ID 为 " + orderId + " 的订单取消失败，请联系工作人员进行处理");
        }
    }

    @Transactional(rollbackFor = ModifyFailureException.class)
    public void handleRefundSuccessResult(String orderId) {
        // 1. 更新订单状态，将订单状态更新为 canceled
        int canceledValue = OrderPaymentStatus.CANCELED.getValue(); // 5

        OrderPayment orderPayment = new OrderPayment();
        orderPayment.setId(orderId);
        orderPayment.setStatus(canceledValue);

        int orderUpdatedCount = orderMapper.updateById(orderPayment);
        if (orderUpdatedCount <= 0) {
            throw new ModifyFailureException("ID 为 " + orderId + " 的订单取消失败，请联系工作人员进行处理");
        }

        log.info("用户取消订单，微信支付平台回调推送取消操作成功，将订单状态更新为 {}, 更新记录条数 {}", canceledValue, orderUpdatedCount);

        // 更新 order_detail 表中 order_id 为 orderId 的订单详情的状态为 canceled
        LambdaUpdateWrapper<OrderDetail> orderDetailUpdateWrapper = new LambdaUpdateWrapper<>();
        orderDetailUpdateWrapper.eq(OrderDetail::getOrderId, orderId);

        OrderDetail orderDetail = new OrderDetail();
        orderDetail.setStatus(canceledValue);

        int orderDetailUpdatedCount = orderDetailMapper.update(orderDetail, orderDetailUpdateWrapper);
        if (orderDetailUpdatedCount <= 0) {
            throw new ModifyFailureException("ID 为 " + orderId + " 的订单取消失败，请联系工作人员进行处理");
        }

        log.info("用户取消订单，微信支付平台回调推送取消操作成功，将订单详情表中，各份菜品的状态更新为 {}, 更新记录条数 {}", canceledValue, orderDetailUpdatedCount);

        // 2. 将 user 表中余额相关的数据进行更新
        updateWalletAfterRefund(orderId);

        // 3. 对库存进行增加
        updateFoodStockAfterRefund(orderId);

        // TODO: 4. 在 transaction_records 中创建退款相关的交易记录
    }

    /**
     * 退还用户余额
     *
     * @param userId         用户 ID
     * @param balance        用户余额
     * @param deductionPrice 用户使用余额抵扣金额
     */
    private void refundUserBalance(String userId, int balance, int deductionPrice) {
        User user = new User();
        user.setId(userId);
        user.setBalance(balance + deductionPrice);
        if (userMapper.updateById(user) <= 0) {
            throw new ModifyFailureException("用户余额退还失败，请联系工作人员进行处理");
        }
    }

    public Result<?> abortPay(String orderId) {
        return wxPayServiceImpl.closeTransaction(orderId);
    }

    /**
     * 1. 将 jx_user_transactions 表中，充值记录的状态从 toPay 更改为 done
     * 2. 将 jx_user 表中，用户余额添加 jx_user_transactions.value2 值
     */
    @Transactional(rollbackFor = Exception.class)
    public void handleRechargeSuccessResult(String userId, String transactionId, Integer value2) {
        // TODO 将 user 表中，用户余额添加 transaction_record.value2 值
//        User userInfo = dataCacheService.getUserInfoFromRedisOrDb(userId);
//        Integer updatedWalletBalance = userInfo.getWalletBalance() + value2;
//
//        // 将 jx_user_transactions 表中，充值记录的状态从 toPay 更改为 done
//        JxUserTransactionsDo jxUserTransactionsDo = new JxUserTransactionsDo();
//        jxUserTransactionsDo.setId(transactionId);
//        jxUserTransactionsDo.setWalletBalance(updatedWalletBalance);
//        jxUserTransactionsDo.setState(JxUserTransactionStateEnum.DONE);
//        jxUserTransactionsMapper.updateById(jxUserTransactionsDo);
//
//        JxUserDo jxUserDo1 = new JxUserDo();
//        jxUserDo1.setId(userId);
//        jxUserDo1.setWalletBalance(updatedWalletBalance);
//        // 先更新数据库
//        jxUserMapper.updateById(jxUserDo1);
//        // 再更新缓存
//        redisUtil.hset(JxRedisConstant.JX_USER_KEY  + userId, "walletBalance", updatedWalletBalance);
    }

    /**
     * 将购物车中的菜品标记删除，并回填到购物项的 orderId
     *
     * @param orderId         订单 ID
     * @param shoppingCartIds 购物车菜品项 ID 列表
     */
    @Transactional(rollbackFor = ModifyFailureException.class)
    public void deleteShoppingCartItemsByFlag(String orderId, List<String> shoppingCartIds) {
        ShoppingCart jxShoppingCartFoodDo = new ShoppingCart();
        jxShoppingCartFoodDo.setOrderId(orderId);
        jxShoppingCartFoodDo.setDelFlag(true);

        LambdaUpdateWrapper<ShoppingCart> updateWrapper = new LambdaUpdateWrapper<ShoppingCart>().in(ShoppingCart::getId, shoppingCartIds);
        if (shoppingCartMapper.update(jxShoppingCartFoodDo, updateWrapper) <= 0) {
            throw new ModifyFailureException("更新购物车状态失败，请联系工作人员进行处理");
        }
        log.info("下单成功，将购物车中的购物项状态设置为已删除，并回填订单id: {}", orderId);
    }

    /**
     * 向数据库中新增订单支付及详情相关信息
     *
     * @param userId        用户 ID
     * @param companyId     员工所属公司 ID，结算时应以员工所属公司进行结算
     * @param shoppingCarts 购物车详情
     * @param dto           订单创建 DTO
     * @return 新创建的订单 ID
     */
    private OrderPayment insertOrderRecords(String userId, String companyId, List<ShoppingCart> shoppingCarts, OrderCreateDTO dto) {
        // 将前端传递过来的金额的单位由元转换为分
        int totalPrice = CurrencyUtil.yuanToFen(dto.getTotalPrice());
        // 前端通过 showPrice 计算的购物车总价
        int totalShowPrice = CurrencyUtil.yuanToFen(dto.getTotalShowPrice());
        if (totalPrice != totalShowPrice) {
            log.info("companyId: {}, phone: {} 的正常菜品销售价格被隐藏，使用 show_price 计算订单金额", dto.getCompanyId(), dto.getPhone());
        }
        // 用户通过余额支付的金额
        int walletPay = CurrencyUtil.yuanToFen(dto.getWalletPay());
        // 使用余额抵扣金额，单位为分
        int deductionPay = 0;
        // 用户自费金额，单位为分，当钱包余额还有金额时，用户自费金额为 0
        int userPaid = 0;
        JxPaymentMethodEnum payMethod;
        if (walletPay >= totalShowPrice) {
            deductionPay = totalShowPrice;
            payMethod = JxPaymentMethodEnum.WALLET;
        } else if (walletPay > 0) {
            deductionPay = walletPay;
            userPaid = totalShowPrice - walletPay;
            payMethod = JxPaymentMethodEnum.WALLET_WXPAY;
        } else {
            userPaid = totalShowPrice;
            payMethod = JxPaymentMethodEnum.WXPAY;
        }

        OrderPayment jxOrderDoPayment = new OrderPayment();
        jxOrderDoPayment.setUserId(userId);
        jxOrderDoPayment.setPhone(dto.getPhone());

        // UPDATE 2025-09-01
        // 下单时 OrderPayment 和 OrderDetail 中记录的公司 ID 为用户下单时公司 ID 而非公司所属 ID
        // 后续如果要判断订单结算归属公司，需要通过 userId 查询 user.company_id 来获取该笔订单实际属于哪个公司

        // - dto.getCompanyId() 为用户下单时的公司
        // - user.getCompanyId() 为员工所属公司
        String companyIdForCreateOrder = dto.getCompanyId();
        if (companyId.equals(dto.getCompanyId())) {
            // 如果员工所属公司 ID 与前端传递的公司 ID 一致，说明当前员工没有出差，就在本公司点的餐
            log.info("insertOrderRecords, 员工尚未出差，在本公司点餐, user.companyId: {}, 前端传递的 companyId: {}", companyId, dto.getCompanyId());
        } else {
            // 如果员工所属公司 ID 与前端传递的公司 ID 不一致，说明当前员工正在其它公司出差
            log.info("insertOrderRecords, 员工正在出差, user.companyId: {}, 前端传递的 companyId: {}", companyId, dto.getCompanyId());
        }

        jxOrderDoPayment.setCompanyId(companyIdForCreateOrder);
        jxOrderDoPayment.setPaymentMethod(payMethod.getValue());
        jxOrderDoPayment.setTotalAmount(totalPrice);

        // TODO: 这里可能会有问题，当前架构下，允许用户同时在多家店铺点餐，那么查询出来的菜品店铺配置信息就可能会有问题
        // 获取用餐时段
        String mealType = shoppingCarts.get(0).getMealType();
        String storeId = shoppingCarts.get(0).getStoreId();
        LambdaQueryWrapper<MenuStoreConfig> queryWrapper = new LambdaQueryWrapper<MenuStoreConfig>()
                .eq(MenuStoreConfig::getCompanyId, dto.getCompanyId())
                .eq(MenuStoreConfig::getMenuId, dto.getMenuId())
                .eq(MenuStoreConfig::getStoreId, storeId)
                .eq(MenuStoreConfig::getMealType, mealType)
                .eq(MenuStoreConfig::getDelFlag, false);
        List<MenuStoreConfig> menuStoreConfigs = menuStoreConfigMapper.selectList(queryWrapper);
        if (menuStoreConfigs == null || menuStoreConfigs.isEmpty()) {
            log.info("/insertOrderRecords, companyId: {}, menuId: {}, storeId: {}, mealType: {}", dto.getCompanyId(), dto.getMenuId(), storeId, mealType);
            throw new IllegalArgumentException("该公司的菜品配置有问题，请联系管理员排查");

        }
        MenuStoreConfig menuStoreConfig = menuStoreConfigs.get(0);

        // 修改订单表中的批量打印标志位
        LocalDateTime createOrderTime = LocalDateTime.now();

        // 判断该订单属于哪一天
        String dateFlag = buildDateFlag(createOrderTime, menuStoreConfig.getStartTime(), menuStoreConfig.getEndTime());
        // 如果当前下单用户的手机号是租户用来加餐的账号，则订单 dateFlag 使用 开始点餐时间和 送达时间来进行判断; 因为 加餐通常在 点餐截止之后发生，如果直接使用现有的 buildDateFlag(), 则生成的 dateFlag 会是订单实际归属日期的第二天
        if (StringUtils.isNoneBlank(canOrderAllTimePhoneListStr) && canOrderAllTimePhoneListStr.contains(dto.getPhone().trim())){
            dateFlag = buildDateFlagForOrderAfterStoreEndTime(createOrderTime, menuStoreConfig.getStartTime(), menuStoreConfig.getDeliveryTime());
        }

        jxOrderDoPayment.setDateFlag(dateFlag);
        log.info("当前订单下单时间为:{}, 属于 {} 日期的订单", createOrderTime, dateFlag);

        // 拼接餐食类型，格式: "午餐 15:00 ~ 11:00"
        String mealTypeChineseName = getMealTypeChineseNameFromEnum(menuStoreConfig.getMealType());
        String mealFlag = String.format("%s %s ~ %s", mealTypeChineseName, menuStoreConfig.getStartTime(), menuStoreConfig.getEndTime());
        jxOrderDoPayment.setMealFlag(mealFlag);
        log.info("该订单为:【{}】", mealFlag);

        // 如果启用批量打印，将该订单的批量打印状态标志置为未打印状态
        if (PrintMethodEnum.BATCH.getName().equals(menuStoreConfig.getPrintType())) {
            jxOrderDoPayment.setBatchPrinted(BatchPrintEnum.NOT_BATCH_PRINTED.getValue());
        } else {
            jxOrderDoPayment.setBatchPrinted(BatchPrintEnum.ONLY_SINGLE_PRINT_SUPPORT.getValue());
        }

        // 如果当前下单用户的手机号是租户用来加餐的账号，则订单立即打印，因此，将打印方式设置为 ONLY_SINGLE_PRINT_SUPPORT, 表示此订单为 一单已打印的订单，而不是由定时任务进行批量打印
        if (StringUtils.isNoneBlank(canOrderAllTimePhoneListStr) && canOrderAllTimePhoneListStr.contains(dto.getPhone().trim())){
            jxOrderDoPayment.setBatchPrinted(BatchPrintEnum.ONLY_SINGLE_PRINT_SUPPORT.getValue());
        }

        // TODO: 从 Redis 中查询应该付给对应店铺的金额
        jxOrderDoPayment.setPayForMerchant(0);
        jxOrderDoPayment.setRefundId(null); // 如果申请退货，在微信支付平台对应的 退款id
        jxOrderDoPayment.setStatus(OrderPaymentStatus.WAIT.getValue()); // 待支付
        jxOrderDoPayment.setDeductionPrice(deductionPay); // 若使用余额抵扣订单金额，则此字段存放抵扣部分金额，单位为分
        jxOrderDoPayment.setUserPaid(userPaid);
        // 向 order_payment 表中添加一条记录
        orderMapper.insert(jxOrderDoPayment);

        for (ShoppingCart shoppingCart : shoppingCarts) {
            OrderDetail orderDetailDo = new OrderDetail();
            orderDetailDo.setOrderId(jxOrderDoPayment.getId());
            orderDetailDo.setCompanyId(companyIdForCreateOrder);
            orderDetailDo.setUserId(userId);
            orderDetailDo.setUsername(dto.getNameTakingMeal());
            orderDetailDo.setPickupAddress(dto.getAddress());

            JSONArray specifications = JSONArray.parseArray(shoppingCart.getSpecification());
            String printName = buildPrintNameWithFoodNameAndSpecifications(shoppingCart.getFoodName(), specifications);

            orderDetailDo.setStoreId(shoppingCart.getStoreId());
            orderDetailDo.setFoodId(shoppingCart.getFoodId());
            // 设置该菜品是那个用餐时段的
            orderDetailDo.setMealType(shoppingCart.getMealType());
            // 小票上打印的菜品名称新格式，包含菜品名和规格
            orderDetailDo.setFoodPrintName(printName);
            // 如果是一份菜品，则为该菜品的销售单价，若用户订购了多份同样菜品，则为 多份菜品的总销售价 = 菜品单价 * 菜品份数
            orderDetailDo.setSalePrice(shoppingCart.getSalePrice());
            // 菜品的展示价格，注意：shopping_cart 表中记录的 show_price 为单份菜品的显示价格
            orderDetailDo.setShowPrice(shoppingCart.getShowPrice());
            orderDetailDo.setQuantity(shoppingCart.getCount());
            orderDetailDo.setFoodSpecification(shoppingCart.getSpecification());
            // 用户填写的备注（TODO: 这个字段是否放到 order_payment 表中更合适呢）
            orderDetailDo.setNote(dto.getNote());
            // 此时还未支付，订单状态还是 待支付
            orderDetailDo.setStatus(OrderPaymentStatus.WAIT.getValue());
            // TODO: 从 Redis 中查询应该付给对应店铺的金额,从 food_menu 读取给店铺的金额而不是 store表；同一个店铺的菜品给不同公司配置的品质，份量，服务方式不同，给店铺的合作价也不同

            // TODO: 将 pickUpCode 放到 redis 中

            // 取餐码默认递增
            Integer pickUpCode = ++pickupCodeCounter;
            if (companyPickupCodeConfigProperties.getEnabledIds().contains(dto.getCompanyId())) {
                // 如果下单用户所在公司启用了取餐码核销功能，则 4 位生成取餐码
                pickUpCode = ThreadLocalRandom.current().nextInt(1000, 10000);
            }
            orderDetailDo.setPickupCode(pickUpCode);

            // 为订单中的每个菜品，向 jx_order_detail 表中添加一条记录
            orderDetailMapper.insert(orderDetailDo);
        }
        return jxOrderDoPayment;
    }

    /**
     * 将数据库中保存的用餐时段的英文名称转换成对应的中文名称
     *
     * @param mealTypeEn 用餐时段的英文名称
     * @return 中文名称
     */
    private String getMealTypeChineseNameFromEnum(String mealTypeEn) {
        return Arrays.stream(MealTypeEnum.values())
                .filter(mealTypeEnum -> mealTypeEnum.getMealTypeEn().equals(mealTypeEn))
                .map(MealTypeEnum::getMealTypeCn)
                .findAny()
                .orElse("");
    }

    private String getMealTypeEnglishNameFromEnum(String mealTypeCn) {
        return Arrays.stream(MealTypeEnum.values())
                .filter(mealTypeEnum -> mealTypeEnum.getMealTypeCn().equals(mealTypeCn))
                .map(MealTypeEnum::getMealTypeEn)
                .findAny()
                .orElse("");
    }

    /**
     * 判断 HH:mm 格式的时间是否在 startTime 到 endTime 之间
     *
     * @param time      HH:mm 格式的时间
     * @param startTime 开始时间，开始时间 > 结束时间时表明该开始时间是从前一天开始计算
     * @param endTime   结束时间
     * @return 在 startTime 到 endTime 之间时，返回 true
     */
    public static boolean isTimeBetween(String time, String startTime, String endTime) {
        // 解析时间字符串为 LocalTime 对象
        DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm");
        LocalTime startlocalTime = LocalTime.parse(startTime, timeFormatter);
        LocalTime endLocalTime = LocalTime.parse(endTime, timeFormatter);
        LocalTime localTime = LocalTime.parse(time, timeFormatter);

        // 获取今天和昨天的日期
        LocalDate today = LocalDate.now();
        LocalDate yesterday = today.minusDays(1);

        // 创建 LocalDateTime 对象
        LocalDateTime startLocalDateTime = LocalDateTime.of(today, startlocalTime);
        LocalDateTime endLocalDateTime = LocalDateTime.of(today, endLocalTime);
        LocalDateTime dateTime = LocalDateTime.of(today, localTime);

        // 如果 startTime 在 b 之后，说明 endTime 是前一天的时间
        if (startlocalTime.isAfter(endLocalTime)) {
            startLocalDateTime = LocalDateTime.of(yesterday, startlocalTime);
        }

        return (dateTime.isAfter(startLocalDateTime) || dateTime.equals(startLocalDateTime)) &&
                (dateTime.isBefore(endLocalDateTime) || dateTime.equals(endLocalDateTime));
    }

    public static boolean isTimeBetween(LocalDateTime time, String startTime, String endTime) {
        // 解析时间字符串为 LocalTime 对象
        DateTimeFormatter startTimeFormatter = DateTimeFormatter.ofPattern("HH:mm");
        DateTimeFormatter endTimeFormatter = DateTimeFormatter.ofPattern("HH:mm");
        // 由于数据库中保存的时间数据可能不统一
        // 默认按 HH:mm 进行解析，如果数据库中保存的时间长度未 8，例如：10:30:00，则按照 HH:mm:ss 解析
        if (startTime.length() == 8) {
            startTimeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss");
        }
        if (endTime.length() == 8) {
            endTimeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss");
        }
        LocalTime startlocalTime = LocalTime.parse(startTime, startTimeFormatter);
        LocalTime endLocalTime = LocalTime.parse(endTime, endTimeFormatter);

        LocalDate today = LocalDate.now();
        LocalDateTime startLocalDateTime = null;
        LocalDateTime endLocalDateTime = null;
        if (endLocalTime.isAfter(startlocalTime)) {
            // 同一天， 不跨天，只看HH:mm 时分，当设置的供餐时间，结束时间比开始时间晚，则开始时间和结束时间没有跨天，在同一天
            // 以开始时间为 14:00，结束时间为 18:00 为例子，此处的开始时间和结束时间都表示当天的14:00 和 18:00
            startLocalDateTime = LocalDateTime.of(today, startlocalTime);
            endLocalDateTime = LocalDateTime.of(today, endLocalTime);
            log.info("计算有效开始时间和结束花时间，在同一天，供餐开始: {}, 供餐结束: {}，有效开始时间: {}, 有效结束时间: {}"
                    , startlocalTime, endLocalTime, startLocalDateTime, endLocalDateTime);
        } else {
            // 跨天，不在同一天，只看HH:mm 时分，当设置的供餐时间，结束时间比开始时间早，则开始时间和结束时间有跨天，不在同一天
            // 以开始时间为 13:00，结束时间为 10:00 为例子，此处的开始时间表示的是前一天的13:00，结束时间为当天的10:00
            LocalTime now = time.toLocalTime();
            // 若 下单时间 在 开始时间 之后，比如用户在 13:20 下单，在此处的 13:00 之后，则 表示是在前一天下单（仅判断 小时 会在 hour 相同时出现问题）
            if (now.isAfter(startlocalTime)) {
                // 用餐前一天下单
                // 开始时间的 天，是当天
                startLocalDateTime = LocalDateTime.of(today, startlocalTime);
                // 结束时间的 天，是明天
                LocalDate tomorrow = today.plusDays(1);
                endLocalDateTime = LocalDateTime.of(tomorrow, endLocalTime);
            } else {
                // 用餐当天下单
                // 开始时间的 天，是前一天
                LocalDate yesterday = today.minusDays(1);
                startLocalDateTime = LocalDateTime.of(yesterday, startlocalTime);
                // 结束时间的 天，是当天
                endLocalDateTime = LocalDateTime.of(today, endLocalTime);
            }
            log.info("计算有效开始时间和结束花时间，跨天，供餐开始: {}, 供餐结束: {}，有效开始时间: {}, 有效结束时间: {}"
                    , startlocalTime, endLocalTime, startLocalDateTime, endLocalDateTime);
        }

        boolean result = (time.isAfter(startLocalDateTime) || time.equals(startLocalDateTime)) &&
                (time.isBefore(endLocalDateTime) || time.equals(endLocalDateTime));
        log.info("根据订单创建时间，判断是午餐还是晚餐的订单，订单创建时间: {}, 允许点餐开始时间: {}, 允许点餐结束时间: {}," +
                        "包括年月日的开始时间: {}, 结束时间: {}, 判断结果: {}"
                , time, startTime, endTime, startLocalDateTime, endLocalDateTime, result);

        return result;
    }


    /**
     * 根据创建订单的时间，以及公司配置的下单开始时间和下单结束时间，判断该订单属于哪一天，并返回订单所在天的日期
     *
     * @param createOrderTime 订单创建时间
     * @param startTime       下单开始时间
     * @param endTime         下单结束时间
     * @return yyyy-MM-dd 格式的日期
     */
    private String buildDateFlag(LocalDateTime createOrderTime, String startTime, String endTime) {
        // menu_store_ config 表中的时间如果格式是 "hh:mm" 时，则在末尾拼接上 ":00"
        // 将时间格式统一为 "HH:mm:ss" 格式，防止时间格式化出现异常
        if (startTime.length() == 5) {
            startTime += ":00";
        }

        if (endTime.length() == 5) {
            endTime += ":00";
        }

        // 日期格式化模板
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");

        // 解析时间字符串为 LocalTime 对象
        DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss");
        LocalTime startlocalTime = LocalTime.parse(startTime, timeFormatter);
        LocalTime endLocalTime = LocalTime.parse(endTime, timeFormatter);

        LocalDateTime today = LocalDateTime.now();

        if (endLocalTime.isAfter(startlocalTime)) {
            // 如果开始时间和结束时间在同一天，说明开始时间和结束时间不跨天
            // 直接返回创建订单所在日期即可
            return createOrderTime.format(formatter);
        } else {
            LocalTime now = createOrderTime.toLocalTime();
            //  如果开始时间和结束时间垮天
            if (now.isAfter(startlocalTime)) {
                // 如果在用餐前一天下单，则返回创建订单所在日期的后一天的日期即可
                LocalDateTime tomorrow = today.plusDays(1);
                return tomorrow.format(formatter);
            } else {
                // 如果在用餐当天下单，直接返回创建订单所在日期即可
                return createOrderTime.format(formatter);
            }
        }
    }


    private String buildDateFlagForOrderAfterStoreEndTime(LocalDateTime createOrderTime, String startTime, String endTime) {
        // menu_store_ config 表中的时间如果格式是 "hh:mm" 时，则在末尾拼接上 ":00"
        // 将时间格式统一为 "HH:mm:ss" 格式，防止时间格式化出现异常
        if (startTime.length() == 5) {
            startTime += ":00";
        }

        if (endTime.length() == 5) {
            endTime += ":00";
        }

        // 日期格式化模板
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");

        // 解析时间字符串为 LocalTime 对象
        DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss");
        LocalTime startlocalTime = LocalTime.parse(startTime, timeFormatter);
        LocalTime endLocalTime = LocalTime.parse(endTime, timeFormatter);

        LocalDateTime today = LocalDateTime.now();

        if (endLocalTime.isAfter(startlocalTime)) {
            // 如果开始时间和结束时间在同一天，说明开始时间和结束时间不跨天
            // 直接返回创建订单所在日期即可
            return createOrderTime.format(formatter);
        } else {
            LocalTime now = createOrderTime.toLocalTime();
            //  如果开始时间和结束时间垮天
            if (now.isAfter(startlocalTime)) {
                // 如果在用餐前一天下单，则返回创建订单所在日期的后一天的日期即可
                LocalDateTime tomorrow = today.plusDays(1);
                return tomorrow.format(formatter);
            } else {
                // 如果在用餐当天下单，直接返回创建订单所在日期即可
                return createOrderTime.format(formatter);
            }
        }
    }

    /**
     * 使用 飞鹅云打印机 或 得力蓝牙打印机进行打印
     *
     * @param orderId 订单 ID
     */
    public void printOrder(String orderId) {
        String printerType = "feie"; // 飞鹅 wifi打印机
        if (printerType == null || printerType.isEmpty() || printerType.equalsIgnoreCase("disable")) {
            // 不打印
            log.warn("系统配置项量 printer_type 取值为: {}，取值无效，系统配置项没有配置，请联系管理人员进行处理", printerType);
        } else if (printerType.equalsIgnoreCase("feie")) {
            log.info("系统配置项量 printer_type 取值为: {}，使用飞鹅云打印机进行打印", printerType);
            // 使用飞鹅云打印机进行打印
            try {
                printOrderWithFeiEyunPrinter(orderId, new HashMap<>());
            } catch (Throwable ex) {
                log.error("手动单独触发单独订单打印失败，订单id：{}", orderId);
                log.error("{}", ex);
            }
        } else if (printerType.equalsIgnoreCase("deli")) {
            // 不打印，得力
            log.debug("系统配置项量 printer_type 取值为: {}，由商户手机微信小程序进行打印", printerType);
        }
    }

    /**
     * 当用户支付完成后，将 jx_order 和 jx_order_detail 表中的订单状态都更新成 toSend
     *
     * @param orderId 订单 ID
     */
    @Transactional(rollbackFor = ModifyFailureException.class)
    public void updateOrderState(String orderId) {
        log.info("支付完成，对订单 {} 状态、余额进行更新", orderId);

        // 支付成功后，订单状态变更为 2，待发货
        OrderPayment jxOrderDoPayment = new OrderPayment();
        jxOrderDoPayment.setId(orderId);
        jxOrderDoPayment.setStatus(OrderPaymentStatus.TO_SEND.getValue());
        if (orderMapper.updateById(jxOrderDoPayment) <= 0) {
            throw new ModifyFailureException("订单状态更新失败，订单 id: " + orderId);
        }

        // 支付成功后，将 order_detail 表中的订单状态变更为 TO_SEND，待发货
        OrderDetail jxOrderDetailDo = new OrderDetail();
        jxOrderDetailDo.setStatus(OrderPaymentStatus.TO_SEND.getValue());
        LambdaUpdateWrapper<OrderDetail> updateWrapper = new LambdaUpdateWrapper<OrderDetail>().eq(OrderDetail::getOrderId, orderId);
        if (orderDetailMapper.update(jxOrderDetailDo, updateWrapper) <= 0) {
            throw new ModifyFailureException("订单详情更新失败，订单 id: " + orderId);
        }
    }


    /**
     * 取消订单，微信支付平台推出 订单取消成功回调后，将 余额，积分 进行恢复
     *
     * @param orderId 订单 ID
     */
    public void updateWalletAfterRefund(String orderId) {

        log.info("用户取消订单 {}, 根据用户支付方式，对用户余额进行恢复", orderId);

        OrderPayment jxOrderDoPayment = orderMapper.selectById(orderId);
        if (jxOrderDoPayment == null) {
            throw new ModifyFailureException("订单不存在，请联系工作人员进行处理。订单 id: " + orderId);
        }

        // 仅当用户使用的是【余额 + 微信支付】的方式付款时，才需要在调用微信退款接口后返还 user 表中的金额
        if (Objects.equals(jxOrderDoPayment.getPaymentMethod(), JxPaymentMethodEnum.WALLET_WXPAY.getValue())) {
            User jxUserDo = userMapper.selectById(jxOrderDoPayment.getUserId());

            refundUserBalance(jxUserDo.getId(), jxUserDo.getBalance(), jxOrderDoPayment.getDeductionPrice());
        }
    }

    public void updateFoodStockAfterRefund(String orderId) {
        log.info("用户取消订单 {}, 对订单涉及到的商品的库存进行恢复", orderId);
        LambdaUpdateWrapper<ShoppingCart> wrapper = new LambdaUpdateWrapper<ShoppingCart>()
                .eq(ShoppingCart::getOrderId, orderId);

        List<ShoppingCart> shoppingCarts = shoppingCartMapper.selectList(wrapper);

        if (shoppingCarts != null && !shoppingCarts.isEmpty()) {
            for (ShoppingCart shoppingCart : shoppingCarts) {
                // 用 menuId 拼接成 redis 中菜品库存的 key
                // store_xxx_stock -> {"food1": 10, "food2":20, "food3": 3, "food4": 0};
                String key = "store_" + shoppingCart.getMenuId() + "_stock";
                // TODO: 确定 redis 中存入的菜品名称命名方式，shopping_cart 表中的 food_print_name 字段只有规格拼接，没有菜品名称
                // 暂定菜品名称为 shopping_cart 中 food_name 和 food_print_name 的拼接
                String item = shoppingCart.getFoodName() + shoppingCart.getFoodPrintName();

                log.info("redis 中获取菜品库存，key: {}, item: {}", key, item);

                Integer limitCount = (Integer) redisUtil.hget(key, item);
                if (limitCount != null) {
                    redisUtil.hset(key, item, limitCount + shoppingCart.getCount());
                }
            }
        }
    }

    /**
     * 订单可能是通过【余额】，【余额+微信】等方式进行支付，当通过【余额】，【余额+微信】支付完成后，需要减除用户的余额
     */
    @Transactional(rollbackFor = ModifyFailureException.class)
    public void updateWalletAfterOrderPayed(OrderPayment orderPayment, User savedUser) {

        if (Objects.equals(orderPayment.getPaymentMethod(), JxPaymentMethodEnum.WALLET.getValue()) ||
                Objects.equals(orderPayment.getPaymentMethod(), JxPaymentMethodEnum.WALLET_WXPAY.getValue())) {
            // 使用【余额全额支付】或使用【余额+ 微信支付】时，将用户余额减去置顶金额
            int updatedWalletBalance = savedUser.getBalance() - orderPayment.getDeductionPrice();
            if (updatedWalletBalance < 0) {
                updatedWalletBalance = 0;
                log.error("抵扣余额后，用户剩余余额小于 0，需要排除下那里是否有 bug");
            }
            User user = new User();
            user.setId(savedUser.getId());
            user.setBalance(updatedWalletBalance);

            if (userMapper.updateById(user) <= 0) {
                throw new ModifyFailureException("更新用户余额失败，请联系工作人员进行处理。用户 id: " + savedUser.getId());
            }
        }
    }

    /**
     * 在 transaction_records 中创建支付相关的交易记录
     *
     * @param orderPayment 订单信息
     * @param user         用户信息
     */
    @Transactional(rollbackFor = ModifyFailureException.class)
    public void createTransactionRecords(OrderPayment orderPayment, User user) {
        // TODO: 假设平台账户 ID 为 32 个 0（暂定，待讨论）
        final String platformId = "00000000000000000000000000000000";
        String paymentMethod = orderPayment.getPaymentMethod();
        String companyId = user.getCompanyId();
        String userId = user.getId();
        String orderId = orderPayment.getId();

        if (Objects.equals(paymentMethod, JxPaymentMethodEnum.WALLET.getValue())) {
            // 仅通过余额支付，创建 1 条交易记录
            createPaymentTransactionRecord(companyId, userId, platformId, orderId,
                    TransactionTypeEnum.WALLET_PAY.getValue(),
                    orderPayment.getDeductionPrice());
        } else if (Objects.equals(paymentMethod, JxPaymentMethodEnum.WALLET_WXPAY.getValue())) {
            // 通过余额+微信支付，创建 2 条交易记录
            createPaymentTransactionRecord(companyId, userId, platformId, orderId,
                    TransactionTypeEnum.WALLET_PAY.getValue(),
                    orderPayment.getDeductionPrice());
            createPaymentTransactionRecord(companyId, userId, platformId, orderId,
                    TransactionTypeEnum.WX_PAY.getValue(),
                    orderPayment.getUserPaid());
        } else if (Objects.equals(paymentMethod, JxPaymentMethodEnum.WXPAY.getValue())) {
            // 仅通过微信支付，创建 1 条交易记录
            createPaymentTransactionRecord(companyId, userId, platformId, orderId,
                    TransactionTypeEnum.WX_PAY.getValue(),
                    orderPayment.getUserPaid());
        } else {
            log.error("支付方式不支持，请联系工作人员进行处理。支付方式: {}", paymentMethod);
        }
    }


    /**
     * 创建一条新的用于记录支付金额的交易记录
     *
     * @param companyId       公司 ID
     * @param userId          用户 ID
     * @param platformId      平台账户 ID
     * @param orderId         订单 ID
     * @param transactionType 交易类型
     * @param amount          交易金额
     */
    private void createPaymentTransactionRecord(String companyId, String userId, String platformId, String orderId, Integer transactionType, Integer amount) {
        TransactionRecord record = new TransactionRecord();
        record.setCompanyId(companyId);
        record.setFromId(userId);
        record.setToId(platformId);
        record.setOrderId(orderId);
        record.setAmount(amount);
        record.setTransactionType(transactionType);
        if (transactionRecordMapper.insert(record) <= 0) {
            throw new ModifyFailureException("创建交易记录失败，请联系工作人员进行处理。");
        }
    }

    /**
     * 将规格中的各个菜品项进行拼接，对于自选快餐，最终打印在标签纸上的菜品名是 各个规格项拼接的结果，而非该自旋快餐 菜品名的 名称
     *
     * @param specification 菜品规格
     * @return 餐品打印名称
     */
    public String buildFoodPrintNameFromSpecification(JSONArray specification) {

        List<String> itemNames = new ArrayList<>();
        for (int k = 0; k < specification.size(); k++) {
            JSONObject specificationItem = specification.getJSONObject(k);
            itemNames.add(specificationItem.getString("name"));
        }

        return String.join("+", itemNames);
    }

    /**
     * 根据订单的创建时间，计算出订单可以取消的时间，返回的时间格式为  yyyy-MM-dd HH:mm:ss
     *
     * @param createTime
     * @param storeSupplyTimeSpan
     * @return
     */
    public String calcOrderCanCancelTime(Date createTime, String storeSupplyTimeSpan) {

        JSONObject supplyTimeSpan = JSONObject.parseObject(storeSupplyTimeSpan);
        String createTimeStr = "";

//                JxUserUtil.formateHHMMSS(createTime);

        String beginTime = supplyTimeSpan.getString("beginTime");
        String endTime = supplyTimeSpan.getString("endTime");
        Date supplyEndTime = null;
//                JxUserUtil.parseDateFromHHMMSS(endTime);
        Calendar supplyEndTimeCal = Calendar.getInstance();
        supplyEndTimeCal.setTime(supplyEndTime);

        Calendar canCancelTimeCal = Calendar.getInstance();
        canCancelTimeCal.setTime(createTime);
        canCancelTimeCal.set(Calendar.HOUR_OF_DAY, supplyEndTimeCal.get(Calendar.HOUR_OF_DAY));
        canCancelTimeCal.set(Calendar.MINUTE, supplyEndTimeCal.get(Calendar.MINUTE));
        canCancelTimeCal.set(Calendar.SECOND, supplyEndTimeCal.get(Calendar.SECOND));

        if (createTimeStr.compareTo(beginTime) > 0) {
            // 供餐开始时间是前一天晚上19:00， 因此，若按照字典序比较，若下单时间 大于 开始供餐时间，则下单时间一定在前一天晚上，因此，可以取消订单的时间，日期需要加1天
            canCancelTimeCal.add(Calendar.DAY_OF_MONTH, 1);
        }

//        return JxUserUtil.formateDateTimeYYYYMMDDHHMMSS(canCancelTimeCal.getTime()) ;
        return "";
    }

    /**
     * 根据站点id，获取该站点内所有店铺的默认 营业起始时间和截止时间
     *
     * @param siteId
     * @return
     */
    public JSONObject getSiteSupplyTimespan(String siteId) {

        // todo
        return null;
    }


    /**
     * 使用飞鹅云打印机进行打印
     *
     * @param orderId
     */
    public void printOrderWithFeiEyunPrinter(String orderId, HashMap<String, String> alreadyPrintedLockerBoxNoOrderIdMap) {
        // 调用打印机接口
        List<OrderPayment> list1 = orderMapper.selectList(new LambdaQueryWrapper<OrderPayment>().eq(OrderPayment::getId, orderId));
        List<OrderDetail> list2 = orderDetailMapper.selectList(new LambdaQueryWrapper<OrderDetail>().eq(OrderDetail::getOrderId, orderId));
        if (list1 != null && list2 != null) {
            OrderPayment orderPaymentDetail = list1.get(0);
            User userInfo = userMapper.selectById(orderPaymentDetail.getUserId());
            String orderTotalPrice = String.valueOf(orderPaymentDetail.getTotalAmount() * 1.0 / 100);
            for (OrderDetail oneFoodDetail : list2) {

                String lockerBoxNo = evalDeliveryBoxNoFromDb(oneFoodDetail, orderPaymentDetail.getDateFlag(), orderPaymentDetail.getMealFlag(), alreadyPrintedLockerBoxNoOrderIdMap);
                // 对订单中的每一个菜品进行打印，订单中的多个菜品，每个菜品可能会重复点多份
                // 订单中每个菜品，可能来自不同的店铺，打印各个菜品小票时，使用各个店铺的打印机信息
                Store storeRepositoryDo = storeMapper.selectById(oneFoodDetail.getStoreId());
                // TODO：是否要在小票上打印菜品的显示价格？
                String printFoodSalePrice = String.valueOf(oneFoodDetail.getSalePrice() * 1.0 / 100);
                String oneFoodTotalPrice = String.valueOf(oneFoodDetail.getSalePrice() * 1.0 / 100); // 一份菜品可能会点多份，多份的总价格, jx_order_detail中的 sale_price已经是多份菜品的总价格了
                String orderCreateTime = orderPaymentDetail.getCreateTime().toString().replace("T", " ");
                if (storeRepositoryDo == null || StringUtils.isBlank(storeRepositoryDo.getPrinterSn())) {
                    log.error("菜品：{} 来自店铺：{}，从店铺仓库表中查询的店铺信息中，打印机序列号无效，值为：{}",
                            oneFoodDetail.getFoodId(), oneFoodDetail.getStoreId(), storeRepositoryDo.getPrinterSn());
                    continue;
                }
                log.info("开始打印，店铺名：{}，打印机SN：{}, printerLabelType: {}",
                        storeRepositoryDo.getName(), storeRepositoryDo.getPrinterSn(), storeRepositoryDo.getPrintLabelType().intValue());
                Department department = departmentMapper.selectById(userInfo.getDepartmentId());
                Company company = companyMapper.selectById(userInfo.getCompanyId());
                if (storeRepositoryDo.getPrintLabelType().intValue() == 1) {
                    log.warn("--- storeName: {}, printerLabelType: {}", storeRepositoryDo.getName(), storeRepositoryDo.getPrintLabelType());
                    // 该店铺使用的是 贴纸打印机，调用贴纸打印机方法
                    // todo 是否可能存在异常，导致订单打印也失败？？？
                    FeieYunPrinterApi.printLabelMsg(storeRepositoryDo.getPrinterSn(), storeRepositoryDo.getName(),
                            department.getName(), userInfo.getUsername(), oneFoodDetail.getFoodPrintName(), oneFoodDetail.getPickupCode(),
                            oneFoodDetail.getNote(), oneFoodDetail.getQuantity(), company.getName(), lockerBoxNo);
                } else {
                    // 单次打印，可能由于菜品名称长度存在特殊场景，导致 分割成多行时出现异常，此处捕获异常，保证其他订单正常打印
                    try {
                        // 该店铺使用的是 小票打印机，调用小票打印机方法
                        String maskedUserPhone = maskUserPhone(userInfo.getPhoneNumber());
                        String responseBodyStr = FeieYunPrinterApi.printTicketTemplate2(
                                storeRepositoryDo.getPrinterSn(),
                                storeRepositoryDo.getName(),
                                oneFoodDetail.getFoodPrintName(),
                                printFoodSalePrice,
                                oneFoodDetail.getQuantity(),
                                oneFoodTotalPrice,
                                oneFoodDetail.getNote(),
                                orderTotalPrice,
                                department.getAddress(),
                                maskedUserPhone,
                                orderCreateTime,
                                oneFoodDetail.getUsername(),
                                oneFoodDetail.getPickupCode(), department.getName());
                        JSONObject responseBody = JSONObject.parseObject(responseBodyStr);
                        Integer retCode = responseBody.getInteger("ret");
                        if (retCode == 0) {
                            log.info("调用云打印机打印标签成功，此时打印机应该正在打印中，等待3秒后调用查询接口，查询打印结果");
                            // 启动异步线程以轮询的方式，调用用云打印机接口，查询订单是否打印完成 (todo 改成线程池)

                        } else {
                            String errMsg = responseBody.getString("msg");
                            log.error("调用云打印机打印标签失败，失败原因: {}", errMsg);
                        }
                        String printOrderId = responseBody.getString("data");
                    } catch (Throwable ex) {
                        log.error("打印异常，订单id: {}, {}", orderId, ex);
                    }
                }

            }
        } else {
            log.error("无法从订单详情表查询到订单中各个菜品的信息，订单id: {}", orderId);
        }
    }

    /**
     * 更新店铺月销量,评分；订单可能会包含多个菜品，因此，需要将订单中每个菜品的销量更新到 对应店铺的 月销量中
     * <p>
     * 当前仅更新了店铺月销量；暂时还没有订单评分功能，因此暂时没有实现 店铺评分的更新
     *
     * @param orderId 订单 ID
     */
    public void updateStoreSalesScores(String orderId) {
        // TODO
    }

    /**
     * 根据 菜品名，用户选择的规格，拼接最后打印在订单中的菜品名称和规格信息，格式如下
     * 菜品名【规格名: 选择的规格】【规格名: 选择的规格】
     * 比如
     * 招聘牛鸡沙拉轻食（主食五选一）【主食: 五常大米】【份量: 中份】
     *
     * @param foodName
     * @param specifications
     * @return
     */
    private String buildPrintNameWithFoodNameAndSpecifications(String foodName, JSONArray specifications) {

        StringBuilder printName = new StringBuilder(64);
        printName.append(foodName);
        printName.append(":");
        // 将用户选择的具体的规格值，添加到list中，使用 String.join("+", list1) 来拼接字符串，代替在for循环中的字符串拼接
        // for循环拼接会在字符串末尾多一个 + 字符，还需要进行单独进行剔除多余的 + 字符
        List<String> selectedSpecificationValues = new ArrayList<>();
        for (int i = 0; i < specifications.size(); i++) {
            String specValue = specifications.getJSONObject(i).getString("name");
            selectedSpecificationValues.add(specValue);
        }

        String specificationValueStr = String.join("+", selectedSpecificationValues);
        printName.append(specificationValueStr);

        return printName.toString();
    }

    /**
     * 根据订单 ID 和用户手机号查询历史购物车信息，并清空当前用户可能存在的购物车数据
     *
     * @param orderId 订单 ID
     * @param phone   用户手机号
     * @return 历史购物车信息
     */
    public List<ShoppingCart> listAndClearShoppingCartByOrderId(String orderId, String phone) {
        LambdaQueryWrapper<ShoppingCart> wrapper = new LambdaQueryWrapper<ShoppingCart>()
                .eq(ShoppingCart::getOrderId, orderId)
                .eq(ShoppingCart::getPhone, phone)
                .eq(ShoppingCart::getDelFlag, true);

        List<ShoppingCart> shoppingCarts = shoppingCartMapper.selectList(wrapper);
        if (shoppingCarts == null || shoppingCarts.isEmpty()) {
            throw new IllegalArgumentException("ID 为 " + orderId + " 的订单中没有菜品信息");
        }

        // 根据 phone + menuId 清空用户当前购物车数据
        Optional<String> menuIdOptional = shoppingCarts.stream().map(ShoppingCart::getMenuId).distinct().findFirst();
        if (menuIdOptional.isPresent()) {
            LambdaUpdateWrapper<ShoppingCart> updateWrapper = new LambdaUpdateWrapper<ShoppingCart>()
                    .eq(ShoppingCart::getPhone, phone)
                    .eq(ShoppingCart::getMenuId, menuIdOptional.get())
                    .eq(ShoppingCart::getDelFlag, false);
            ShoppingCart shoppingCart = new ShoppingCart();
            shoppingCart.setDelFlag(true);

            shoppingCartMapper.update(shoppingCart, updateWrapper);
        }

        return shoppingCarts;
    }

    public void checkIfCouldCreateOrderAgain(List<ShoppingCart> shoppingCarts, String phone) {
        LambdaQueryWrapper<User> queryUserWrapper = new LambdaQueryWrapper<User>()
                .eq(User::getPhoneNumber, phone)
                .eq(User::getDelFlag, false)
                .orderByDesc(User::getCreateTime);
        List<User> users = userMapper.selectList(queryUserWrapper);
        if (users == null || users.isEmpty()) {
            throw new IllegalArgumentException("手机号为 " + phone + " 的用户不存在");
        }

        String companyId = users.get(0).getCompanyId();

        for (ShoppingCart shoppingCart : shoppingCarts) {
            String mealType = shoppingCart.getMealType();
            String menuId = shoppingCart.getMenuId();
            String storeId = shoppingCart.getStoreId();
            LambdaQueryWrapper<MenuStoreConfig> queryMenuStoreConfigWrapper = new LambdaQueryWrapper<MenuStoreConfig>()
                    .eq(MenuStoreConfig::getCompanyId, companyId)
                    .eq(MenuStoreConfig::getMenuId, menuId)
                    .eq(MenuStoreConfig::getStoreId, storeId)
                    .eq(MenuStoreConfig::getMealType, mealType)
                    .eq(MenuStoreConfig::getDelFlag, false)
                    .orderByDesc(MenuStoreConfig::getCreateTime);
            List<MenuStoreConfig> menuStoreConfigs = menuStoreConfigMapper.selectList(queryMenuStoreConfigWrapper);
            if (menuStoreConfigs == null || menuStoreConfigs.isEmpty()) {
                log.error("checkIfCouldCreateOrderAgain, companyId: {}, menuId: {}, storeId: {}, mealType: {}", companyId, menuId, storeId, mealType);
                throw new IllegalArgumentException("该公司菜品配置有问题，请联系管理员排查");
            }
            MenuStoreConfig menuStoreConfig = menuStoreConfigs.get(0);

            boolean timeBetween = isTimeBetween(LocalDateTime.now(), menuStoreConfig.getStartTime(), menuStoreConfig.getEndTime());
            if (!timeBetween) {
                // 如果当前时间不在菜单配置的配送时间内，说明传递过来的 "再来一单" 的数据不符合下单时间要求
                // 返回错误提示消息给前端
                throw new IllegalArgumentException("超出下单截至时间，下单失败");
            }
        }
    }

    /**
     * 将历史购物车数据复制一份插入到数据库中
     */
    @Transactional(rollbackFor = ModifyFailureException.class)
    public List<ShoppingCart> addShoppingCartAgain(List<ShoppingCart> shoppingCarts) {

        List<ShoppingCart> shoppingCartList = new ArrayList<>();

        for (ShoppingCart oldShoppingCart : shoppingCarts) {
            ShoppingCart newShoppingCart = new ShoppingCart();

            newShoppingCart.setUserId(oldShoppingCart.getUserId());
            newShoppingCart.setPhone(oldShoppingCart.getPhone());
            newShoppingCart.setMenuId(oldShoppingCart.getMenuId());
            newShoppingCart.setStoreId(oldShoppingCart.getStoreId());
            newShoppingCart.setStoreName(oldShoppingCart.getStoreName());
            newShoppingCart.setFoodId(oldShoppingCart.getFoodId());
            newShoppingCart.setFoodName(oldShoppingCart.getFoodName());
            newShoppingCart.setFoodImage(oldShoppingCart.getFoodImage());
            newShoppingCart.setFoodPrintName(oldShoppingCart.getFoodPrintName());
            newShoppingCart.setCount(oldShoppingCart.getCount());
            newShoppingCart.setSpecification(oldShoppingCart.getSpecification());
            newShoppingCart.setSalePrice(oldShoppingCart.getSalePrice());
            newShoppingCart.setShowPrice(oldShoppingCart.getShowPrice());
            // 新的购物车记录用餐时段填写复制的购物车记录中的用餐时段
            // 使用此方法前必须判断当前时段是否可以下单！
            newShoppingCart.setMealType(oldShoppingCart.getMealType());

            if (shoppingCartMapper.insert(newShoppingCart) <= 0) {
                throw new ModifyFailureException("再次添加购物车数据失败");
            }

            shoppingCartList.add(newShoppingCart);
        }

        return shoppingCartList;
    }

    /**
     * 隐藏手机号 中间4位， 格式化为 150****1223 格式的字符串
     *
     * @param userPhone
     * @return
     */
    public static String maskUserPhone(String userPhone) {

        if (StringUtils.isNoneEmpty(userPhone) && userPhone.length() == 11) {
            String maskedPhone = MessageFormat.format("{0}****{1}", userPhone.substring(0, 3), userPhone.substring(8));
            return maskedPhone;
        } else {
            return userPhone;
        }
    }

    private String evalDeliveryBoxNoFromDb(OrderDetail orderDetail, String dateFlag, String mealFlag, HashMap<String, String> alreadyPrintedLockerBoxNoOrderIdMap) {

        log.info("before_print_label_or_ticket, eval_locker_box_no, orderId: {}, lockerBoxNo: {}", orderDetail.getOrderId(), orderDetail.getLockerBoxNo());
        String lockerBoxNo = OrderConstants.SHORT_DEFAULT_LOCKER_BOX_NO;
        try {
            if (StringUtils.isBlank(orderDetail.getLockerBoxNo())
                    || OrderConstants.DEFAULT_LOCKER_BOX_NO.equalsIgnoreCase(orderDetail.getLockerBoxNo())
                    || OrderConstants.SHORT_DEFAULT_LOCKER_BOX_NO.equalsIgnoreCase(orderDetail.getLockerBoxNo())) {
                // 对于来自没有外卖柜的公司的订单，货道编号使用 取餐编号; locker_box_no 为 null或空字符串，或者为 32个0，或 00-0-00，则使用取餐编号
                lockerBoxNo = String.valueOf(orderDetail.getPickupCode());
            } else {
                lockerBoxNo = orderDetail.getLockerBoxNo();
                // 如果当订单份数超过外卖柜窗口个数，则不会走此分支；若为重复编号的订单，分配的新订单编号是 默认32个0，则
                if (alreadyPrintedLockerBoxNoOrderIdMap.containsKey(lockerBoxNo)) {
                    String mealTypeEn = evalMealTypeEnByMealFlag(mealFlag);
                    String newLockerBoxNo = assignLockerBoxNoForOrder(orderDetail.getOrderId(), orderDetail.getCompanyId(), dateFlag, mealTypeEn);
                    log.info("evalDeliveryBoxNoFromDbWhenPrintLabel, find duplicated boxNo: {}, companyId: {}, userName: {}, assignedNewLockerBoxNo: {}, orderId: {}, {}",
                            lockerBoxNo, newLockerBoxNo, orderDetail.getCompanyId(), orderDetail.getUsername(), alreadyPrintedLockerBoxNoOrderIdMap.get(lockerBoxNo), orderDetail.getOrderId());
                    lockerBoxNo = newLockerBoxNo;
                }

                // 只有当新分配的外卖柜编号，不等于默认32个0 或 0-0-00 ，才放入到 map中
                if (!OrderConstants.DEFAULT_LOCKER_BOX_NO.equalsIgnoreCase(lockerBoxNo)
                        && !OrderConstants.SHORT_DEFAULT_LOCKER_BOX_NO.equalsIgnoreCase(lockerBoxNo)) {
                    alreadyPrintedLockerBoxNoOrderIdMap.put(lockerBoxNo, orderDetail.getOrderId());
                } else {
                    // 如果重新分配后，分配的编号是 全0，则打印的贴纸上，外卖柜货道编号处打印的是 数字格式的取餐码
                    lockerBoxNo = String.valueOf(orderDetail.getPickupCode());
                }
            }
        } catch (Throwable th) {
            log.error("evalDeliveryBoxNoFromDb excepted, print_00-0-00_as_default, th: ", th);
        }

        return lockerBoxNo;
    }


    public List<DeliverBoxApiParamVO> evaltAvaliableDeliverBoxList(List<DeliveryLocker> bindedDeliveryLockers, List<String> avaliableLockerBoxNoList) {
        List<DeliverBoxApiParamVO> avaliableBoxList = new ArrayList<>();
        int seqNo = 1;
        // 如果有3个货柜，贴标分别为 A，B，D，则，先按照字典顺序排序，生成可分配的编号 A1~A36, B1~B36, D1~D36
        bindedDeliveryLockers.sort(Comparator.comparing(DeliveryLocker::getLockerLabel));
        for (DeliveryLocker locker : bindedDeliveryLockers) {
            log.info("binded locker for companyId: {}, lockerId: {}, startCol: {}, endCol: {}",
                    locker.getCompanyId(), locker.getId(), locker.getColStart(), locker.getColEnd());
            for (int col = locker.getColStart(); col <= locker.getColEnd(); col++) {
                // 某个窗口的编号, A-1, A表示 柜门上的贴标， 1 表示 走上角第一个柜门
                String boxNo = MessageFormat.format("{0}-{1}", locker.getLockerLabel(), col);
                avaliableLockerBoxNoList.add(boxNo);
                DeliverBoxApiParamVO deliverBoxApiParamVO = new DeliverBoxApiParamVO();
                deliverBoxApiParamVO.setLockerBoxNo(boxNo);
                deliverBoxApiParamVO.setAppId(locker.getAppId());
                deliverBoxApiParamVO.setAppKey(locker.getAppKey());
                deliverBoxApiParamVO.setClientNo(locker.getClientNo());
                deliverBoxApiParamVO.setCab(locker.getCab());
                deliverBoxApiParamVO.setCol(col);
                deliverBoxApiParamVO.setSeqNo(seqNo);
                avaliableBoxList.add(deliverBoxApiParamVO);

                seqNo++;
            }
        }
        return avaliableBoxList;
    }

    private String evalMealTypeEnByMealFlag(String mealFlag) {

        String mealType = mealFlag.split(" ")[0];
        // 根据 mealFlag 中的 "午餐" 获取对应的英文名称 "lunch"
        return getMealTypeEnglishNameFromEnum(mealType);
    }
}
